﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using System.Reflection;
using Microsoft.Xna.Framework.Graphics.PackedVector;
using System.Timers;

namespace FrogEngine
{
	/// <summary>
	/// Adapted from Microsofts Particle Sample for XNA 4
	/// </summary>
	/// <typeparam name="T">Your IParticleVertex concrete</typeparam>
	/// <typeparam name="S">Your ParticleSettings concrete</typeparam>
	public abstract class GPUParticleSystem<T, S> : GameEntityBase, IParticleSystem
		where T : struct, IParticleVertex
		where S : IParticleSettings<T>
	{
		/// <summary>
		/// Particle System is one to one with an Effect
		/// </summary>
		/// <param name="engine"></param>
		/// <param name="vertDec"></param>
		public GPUParticleSystem(IEngine engine)
			: base(engine)
		{
			this.particleType = typeof(T);
			var particle = (T)Activator.CreateInstance(particleType);
			var vd = (VertexDeclaration)particleType.InvokeMember("VertexDeclaration", BindingFlags.GetField | BindingFlags.Public | BindingFlags.Static, null, particle, new object[] { });

			Type particleSettingsType = typeof(S);
			this.settings = (S)Activator.CreateInstance(particleSettingsType);

			this.renderService = engine.GetService<RenderService>();
			this.cameraService = engine.GetService<ICameraService>();
			this.updateService = engine.GetService<UpdateEntityService>();
			var particleService = engine.GetService<ParticleService>();
			var contentService = engine.GetService<GameContentService>();
			this.Services.Add(renderService);
			this.Services.Add(updateService);
			this.Services.Add(particleService);
			this.Services.Add(contentService);

			foreach (var service in this.Services)
				service.Subscribe(this);

			vertexBuffer = new DynamicVertexBuffer(engine.GraphicsDevice, particleType, this.settings.MaxParticles * 4, BufferUsage.WriteOnly);
			indexBuffer = new DynamicIndexBuffer(engine.GraphicsDevice, IndexElementSize.ThirtyTwoBits, this.settings.MaxParticles * 6, BufferUsage.WriteOnly);

			vertexData = new T[this.settings.MaxParticles * 4];

			indexData = new int[this.settings.MaxParticles * 6];
			this.InitializeIndices();
		}

		private IRenderEntityService renderService;
		private ICameraService cameraService;
		private IUpdateEntityService updateService;

		private Type particleType;
		protected S settings;
		protected Effect shader;
		protected DynamicVertexBuffer vertexBuffer;
		protected DynamicIndexBuffer indexBuffer;
		private T[] vertexData;
		private int[] indexData;
		private float time = 0;

		public void AddParticle(Vector3 position, Vector3 velocity)
		{
			int nextFree = (firstFreeParticle + 1);
			if (nextFree >= this.settings.MaxParticles)
				nextFree = 0;

			if (nextFree == firstRetiredParticle)
				return;

			this.settings.FillVertices(ref vertexData[firstFreeParticle * 4 + 0],
									   ref vertexData[firstFreeParticle * 4 + 1],
									   ref vertexData[firstFreeParticle * 4 + 2],
									   ref vertexData[firstFreeParticle * 4 + 3], ref position, ref velocity, this.time);

			firstFreeParticle = nextFree;
		}

		private int firstActiveParticle;
		private int firstNewParticle;
		private int firstFreeParticle;
		private int firstRetiredParticle;
		private int drawCounter;

		public virtual void Update(GameTime gameTime)
		{
			time += (float)gameTime.ElapsedGameTime.TotalSeconds;

			this.RetireActiveParticles();
			this.FreeRetiredParticles();

			if (firstActiveParticle == firstFreeParticle)
				time = 0;

			if (firstActiveParticle == firstRetiredParticle)
				drawCounter = 0;
		}

		public virtual void Render(GameTime gameTime)
		{
			GraphicsDevice device = engine.GraphicsDevice;

			// Restore the vertex buffer contents if the graphics device was lost.
			if (vertexBuffer.IsContentLost)
			{
				vertexBuffer.SetData(vertexData);
			}

			// If there are any particles waiting in the newly added queue,
			// we'd better upload them to the GPU ready for drawing.
			if (firstNewParticle != firstFreeParticle)
			{
				AddNewParticlesToVertexBuffer();
			}

			// If there are any active particles, draw them now!
			if (firstActiveParticle != firstFreeParticle)
			{
				device.BlendState = settings.BlendState;
				device.DepthStencilState = DepthStencilState.DepthRead;
				device.RasterizerState = RasterizerState.CullCounterClockwise;

				this.SetShaderParameters(device, cameraService, time);

				// Set the particle vertex and index buffer.
				device.SetVertexBuffer(vertexBuffer);
				device.Indices = indexBuffer;
				// Activate the particle effect.
				foreach (EffectPass pass in shader.CurrentTechnique.Passes)
				{
					pass.Apply();

					if (firstActiveParticle < firstFreeParticle)
					{
						// If the active particles are all in one consecutive range,
						// we can draw them all in a single call.
						device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0,
													 firstActiveParticle * 4, (firstFreeParticle - firstActiveParticle) * 4,
													 firstActiveParticle * 6, (firstFreeParticle - firstActiveParticle) * 2);
					}
					else
					{
						// If the active particle range wraps past the end of the queue
						// back to the start, we must split them over two draw calls.
						device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0,
													 firstActiveParticle * 4, (settings.MaxParticles - firstActiveParticle) * 4,
													 firstActiveParticle * 6, (settings.MaxParticles - firstActiveParticle) * 2);

						if (firstFreeParticle > 0)
						{
							device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0,
														 0, firstFreeParticle * 4,
														 0, firstFreeParticle * 2);
						}
					}
				}

				// Reset some of the renderstates that we changed,
				// so as not to mess up any other subsequent drawing.
				device.DepthStencilState = DepthStencilState.Default;
				device.BlendState = BlendState.Opaque;
			}

			drawCounter++;
		}

		public abstract void SetShaderParameters(GraphicsDevice device, ICameraService cameraService, float systemTimer);

		public ContentManager ContentManager { get; set; }

		public abstract IEnumerable<string> AssetNames();

		public abstract void LoadContent();

		private void InitializeIndices()
		{
			int partStride = 4; // 4 vertices per particle
			for (int j = 0; j < this.settings.MaxParticles; j++)
			{
				int k = j * 6;
				int i = partStride * j;
				indexData[k + 0] = i + 0;
				indexData[k + 1] = i + 1;
				indexData[k + 2] = i + 2;
				indexData[k + 3] = i + 0;
				indexData[k + 4] = i + 2;
				indexData[k + 5] = i + 3;
			}

			indexBuffer.SetData(indexData);
		}

		/// <summary>
		/// Helper for checking when active particles have reached the end of
		/// their life. It moves old particles from the active area of the queue
		/// to the retired section.
		/// </summary>
		private void RetireActiveParticles()
		{
			float particleDuration = settings.LifeTime;

			while (firstActiveParticle != firstNewParticle)
			{
				// Is this particle old enough to retire?
				// We multiply the active particle index by four, because each
				// particle consists of a quad that is made up of four vertices.
				var timefield = particleType.GetField("Time");
				float t = (float)timefield.GetValue(vertexData[firstActiveParticle * 4]);
				float particleAge = time - t;

				if (particleAge < particleDuration)
					break;

				// Remember the time at which we retired this particle.
				timefield.SetValue(vertexData[firstActiveParticle * 4], drawCounter);
				//vertexData[firstActiveParticle * 4].Time = drawCounter;

				// Move the particle from the active to the retired queue.
				firstActiveParticle++;

				if (firstActiveParticle >= settings.MaxParticles)
					firstActiveParticle = 0;
			}
		}

		/// <summary>
		/// Helper for checking when retired particles have been kept around long
		/// enough that we can be sure the GPU is no longer using them. It moves
		/// old particles from the retired area of the queue to the free section.
		/// </summary>
		void FreeRetiredParticles()
		{
			while (firstRetiredParticle != firstActiveParticle)
			{
				// Has this particle been unused long enough that
				// the GPU is sure to be finished with it?
				// We multiply the retired particle index by four, because each
				// particle consists of a quad that is made up of four vertices.
				float t = (float)particleType.InvokeMember("Time", BindingFlags.GetField, null, vertexData[firstRetiredParticle * 4], new object[] { });
				int age = drawCounter - (int)t;

				// The GPU is never supposed to get more than 2 frames behind the CPU.
				// We add 1 to that, just to be safe in case of buggy drivers that
				// might bend the rules and let the GPU get further behind.
				if (age < 3)
					break;

				// Move the particle from the retired to the free queue.
				firstRetiredParticle++;

				if (firstRetiredParticle >= settings.MaxParticles)
					firstRetiredParticle = 0;
			}
		}

		/// <summary>
		/// Helper for uploading new particles from our managed
		/// array to the GPU vertex buffer.
		/// </summary>
		void AddNewParticlesToVertexBuffer()
		{
			Type vertexType = typeof(T);
			var vert = (T)Activator.CreateInstance(vertexType);
			int size = (int)vertexType.InvokeMember("Size", BindingFlags.GetField | BindingFlags.Public | BindingFlags.Static, null, vert, new object[] { });
			int stride = size;

			if (firstNewParticle < firstFreeParticle)
			{
				// If the new particles are all in one consecutive range,
				// we can upload them all in a single call.
				vertexBuffer.SetData(firstNewParticle * stride * 4, vertexData,
									 firstNewParticle * 4,
									 (firstFreeParticle - firstNewParticle) * 4,
									 stride, SetDataOptions.NoOverwrite);
			}
			else
			{
				// If the new particle range wraps past the end of the queue
				// back to the start, we must split them over two upload calls.
				vertexBuffer.SetData(firstNewParticle * stride * 4, vertexData,
									 firstNewParticle * 4,
									 (settings.MaxParticles - firstNewParticle) * 4,
									 stride, SetDataOptions.NoOverwrite);

				if (firstFreeParticle > 0)
				{
					vertexBuffer.SetData(0, vertexData,
										 0, firstFreeParticle * 4,
										 stride, SetDataOptions.NoOverwrite);
				}
			}

			// Move the particles we just uploaded from the new to the active queue.
			firstNewParticle = firstFreeParticle;
		}
	}
}
